Borland Online And The Cobb Group Present:


September, 1995 - Vol. 2 No. 9

Streaming text to a device context

by Nat Ersoz

Microsoft Windows offers a set of Application Programming Interface (API) functions for formatting and preparing text for display to a window. Typically, a Windows programmer will call functions such as TextOut() or ExtTextOut() when writing text into a window's device context­­but using these functions becomes tedious when you need to format data and user-defined objects. For example, to display an int value, a double value, and a user-defined myObject object, you'd need to write code similar to the following:

void TMyWindow::Paint( TDC& dc, bool erase, 
                     TRect& rect )
{
    char buf[80];
    int n, x, y;

// Other Paint code here
n = wsprintf( buf, "%d, %f", myInt, myDouble ); TextOut( dc, x, y, buf, n ); ostrstream os; os << ", " << myObject << ends; TextOut( dc, x, y, buf, strlen(os.str())); delete os.str(); }

Pardon me, but in my humble opinion: Yuck! In order to eliminate the wsprintf() calls and ostrstream intermediate usage, I wrote a class called odcstream. This class allows you to rewrite the above code in the following manner:

void TMyWindow::Paint( TDC& dc, bool erase,
			TRect& rect )
{
    odcstream
        os( dc );
// Other Paint code here

    os << myInt << ", " << myDouble << ", " << myObject << endl;
}

In other words, I output objects to a device context using the rich formatting features provided by the iostream library. In this article, we'll show how you can create and use the odcstream class to provide stream-style output to a device context.

The odcstream and dcstreambuf class structure

The standard stream classes can be broken into two different class structures. The first set of classes defined in IOSTREAM.H derives from the class ios. This set includes istream, which supplies data, and ostream, which receives data. We'll derive the odcstream class from both the ostream class and the TDC class, which defines device context behavior, as shown in Figure A.


Figure A - The odcstream class inherits from the ostream and TDC classes.

The second set of classes defined in IOSTREAM.H derives from the base class streambuf. The ios classes (istream and ostream) usually contain a pointer to a streambuf (or derivative) object, which buffers the data prior to formatting it and outputting it to a device. In simplified terms, one can say that ios-derived classes format the data, and streambuf-derived classes hold the data.

The dcstreambuf class

Since streambuf objects act as temporary containers for data going to or from an ios object, we'll create a dcstreambuf class to buffer the data we want to write to a Windows device context. To do so, we'll construct a structure that manages the elements of the buffer shown in Figure B.


Figure B - Just like the streambuf class, the dcstreambuf class will manage the buffer using five pointers.

As you can see, members base_ and pbase_ both point to the starting address of the character buffer. Conversely, members ebuf_ and epptr_ both point to one element past the end of the character buffer. The member pptr_ points to the location where the stream will place new characters, and the stream increments this pointer each time the program adds a character to the buffer. Of these pointers, pptr_ is the only one that moves. The others remain fixed unless you reallocate the buffer.

We've declared each of these members as private to the streambuf class. To access each respective pointer, you'll use protected member functions that have the same name as each pointer, minus the trailing underscore. For example, to access the member pointer pbase_, the derived streambuf class can call pbase().

As you send data to an odcstream object, the class performs any necessary type conversion and then places the resulting characters into the dcstreambuf object at the location that pptr_ points to. The buffer will continue to fill up until it overflows (when pptr_ exceeds epptr_) or until you call the flush() member function to empty it. When one of these events occurs, the odcstream object sends the contents of the buffer to the output device, which for this class is a Windows device context.

By the way, because we're using the dcstreambuf class only for buffering output to a device context, you'll notice we're not using the gbase_, egptr_, and gptr_ pointers. Stream classes that handle input (those derived from istream) use these members to manage the input buffer.

dcstreambuf::sync( ) operation

The sync() member function is the heart of the dcstreambuf operation. It performs the work of outputting the contents of the buffer to the device context.

If it weren't for the fact that it interprets special characters held within the buffer, this is how the sync() member function would look:

int dcstreambuf::sync()  
{                        
    if( !out_waiting() ) 
        return TRUE;     
unsigned int len = pptr() - pbase(); TextOut( hDC, x, y, pbase(), len ); setp( buf, buf+bufsiz ); return TRUE; }

This very simple dcstreambuf::sync() implementation would succeed in sending the formatted buffer of data to the device context. Note that two data members of dcstreambuf, x and y, provide the position information used by the TextOut() function. The x and y data members determine the horizontal and vertical placement, respectively, of the text.

There are some glaring deficiencies with this implementation of the function dcstreambuf::sync(). One is that successive calls to the sync() function would continuously print the data over itself. To fix this problem, we need to update the value of x every time the program sends data to the device context. To accomplish this, we can insert the following line of code after the TextOut() function call:

// increment x by the length of the string
x += LOWORD( GetTextExtent( hDC, pbase(),
		     len ));

Now, as the other member functions call dcstreambuf::sync(), the horizontal position for text placement in the device context will move to the right. This eliminates the overwriting of previous text, at least in the horizontal direction. (By the way, in the code fragment above, you'll notice that we show a call to the GetTextExtent() API function. We make this call to ensure that we calculate the length of the string correctly when the window is using a proportional font.)

Interpreting special characters in dcstreambuf::sync( )

At this point, we've created a streamable interface that replaces the TextOut() function and does a bit more. It would be an even better interface if the class interpreted special formatting characters such as \n, \t, and \r in an intuitive manner. Let's look through the portion of the buffer that pertains to the sync() function and discuss the special characters we find there.

The newline character

As we search the buffer, if we encounter a newline character (\n), we'll send all contents of the buffer up to this character to the device context. Then, we'll adjust the value of y in order to position the subsequent characters on the next line.

The tab character

If we encounter a tab character (\t) as we search the buffer, we'll need to adjust the horizontal position to produce the effect of a tab operation. I found it useful to define two types of tab interpretation for my own work. The first type involves moving the text insertion point to the next available tab column, based on the last printable character's location. You'll find the next available tab column in the ansitab() member function, and I refer to it as 'ansi'-style tab indenting. This type of tab indenting reflects the way most text editors work.

The second type of tab indenting­­which I refer to as 'hard' tabbing­­moves the text insertion point to a location that's defined by how many previous tabs have been used in the current horizontal line. It's implemented in the hardtab() member function. With this type of tab interpretation, it's possible for successive calls to TextOut to write on top of previous calls. For some applications, however, I've found it useful to place text in a specific column, regardless of the previous text length.

The location of tab columns is held in an array called tabs. The size of the tabs array is held in the data member ntabs, and the current tab level depth is held in the integer itab.

The carriage-return character

Finally, the carriage return (\r) would be another useful special character to interpret. In this case, it's conventional to interpret a carriage return as setting the next insertion point to occur at the beginning of the next horizontal line.

The dcstreambuf constructor and destructor

As you may have noticed, the dcstreambuf class performs most of the work involved in streaming data to the device context. Yet it's a class that's, for the most part, hidden from the programmer. All of its members and member functions are protected except for the constructor and destructor.

The odcstream class provides the interface to the Windows programmer and contains a pointer to a dcstreambuf object. This class formats the data (for example, the strings char and int) and places it into the streambuf class. This functionality is already present in the ostream class and all classes that inherit from it. Therefore, the only functions we need to add are those that relate to device-context issues.

odcstream position functions and manipulators

The odcstream class provides several position member functions for setting the text location in the device context. They are odcstream::x( int ), odcstream::y( int ), and odcstream::xy( int, int ). As you'd expect, a call to the odcstream::x( int ) function sets the horizontal position for the text within the device context. Similarly, a call to the odcstream::y( int ) function will set the vertical position in the device context. A call to the function odcstream::xy( int, int ) sets both the horizontal and vertical position.

What's even more interesting is how I've implemented the iostream manipulators. Manipulators allow you to write code using the following form:

ostream << hex << setfill( '0' ) << I >> endl;

In the above code fragment, the tokens hex, setfill, and endl are manipulators. You can use manipulators to modify the ostream (or, in my class, odcstream) member via the left shift (<<) operator. Through the use of the header file IOMANIP.H, you can create your own manipulators. The odcstream class uses manipulators named gotox(), gotoy(), and gotoxy().

Single-argument manipulators

To demonstrate how to implement manipulators, let's examine the gotox manipulator. The DCSTREAM.H and IOMANIP.H header files contain the following code related to implementing the gotox manipulator:

// from the file iomanip.h:
template<class typ> class omanip
{
  private:
    ostream& (*_fn)(ostream&, typ);
    typ _ag;
    
  public:
    	omanip(ostream& (*_f)(ostream&, typ), 
typ _z ) :
 	_fn(_f), _ag(_z) { }

  friend ostream& operator << (ostream& _s, 		omanip<typ>& _f)
  { return(*_f._fn)(_s,_f._ag); }
};

// from the file dcstream.h:
inline ostream& odc_x_( ostream& os, int x )
{
  ((odcstream&)os).x( x );
  return os;
}

inline omanip<int> gotox( int x )
{
  return omanip<int>( odc_x_, x );
}

All of this code is necessary for a simple manipulator. The Borland-supplied file IOMANIP.H contains the class template for omanip. The omanip class has a constructor that requires two arguments: a pointer to a function and an instantiated template-defined value. In the case of odcstream, the template-specific type is an int value, and the argument to the manipulator is gotox(int).

Fortunately, using a manipulator is much easier than writing one. For example, you'll call a manipulator using the following syntax:

odcstream
    os( hdc, dcbuf );       
    // typical constructor stuff

os << gotox( 10 );

When you call gotox( 10 ), it returns a copy of omanip<int>. At this point, the copy of omanip<int> is situated on the right side of the first << operator. After this substitution, the expression looks like this:

os << omanip<int>;

If you look inside the omanip<int> class, you'll see a friend operator that knows how to handle sending omanip<typ> out to the ostream class. Once more, a substitution takes place. The operator calls the function to which the first argument of the omanip stream pointed. This, in fact, is the function odc_x_(), which was the first argument to the omanip<int> constructor. And, finally, odc_x_() calls the odcstream member function x( int ), which then sets the horizontal location for writing text onto the device context.

Using the odcstream class

To use the odcstream class in your own projects, you'll want to include a dcstreambuf object as a member of your TWindow-derived classes. If you examine the TMyWindow::Paint() member function in Listing C, you'll see that I create an odcstream object, passing in a handle to the device context (HDC) and a reference (or pointer) to the dcstreambuf object.


Listing C: STREAMER.CPP

#include 
#include 
#include 
#include "dcstream.h"

const char *text = "\
 This is the first line of text\n\
 This is the second line of text\n\
 Here is a tab\t<-- that's it right there\n\
\tThis line starts with a tab and ends in a return\r\
 xxx\n\
 This is the sixth line of text\n\
 This is the seventh line of text\n\
 This is the eighth line of text ";

class TMyWindow: public TWindow
{ public:
    TMyWindow( void ): TWindow( NULL, "" )
    { Attr.Style |= WS_HSCROLL | WS_VSCROLL;
      HDC hdc;
      TEXTMETRIC tm;

      hdc = GetDC( HWindow );
      GetTextMetrics( hdc, &tm );
      Scroller = new TScroller( this,
             tm.tmAveCharWidth, 
             tm.tmHeight,
             30 * tm.tmAveCharWidth,
             10 * tm.tmHeight );
      ReleaseDC( HWindow, hdc ); };

  protected:
    dcstreambuf dcbuf;

    virtual void Paint( TDC& tdc, BOOL, TRect& )
    { odcstream odc( tdc, dcbuf );
      // Text BkGnd does not overwrite Rectangles
      odc.SetBkMode( TRANSPARENT );
      // use ansi style tabbing
      odc.ansitab();
      odc.tab(8);

      odc << text;}
};

class TMyApp: public TApplication
{ public:
    TMyApp( void ): TApplication() {};

    void InitMainWindow( void )
    { SetMainWindow( new TFrameWindow( 0,
                     "odcstream test",
                     new TMyWindow() ) ); }
};

int OwlMain( int, char*[] )
{
   return TMyApp().Run();
}

You can see how the odcstream class works by creating the three files that appear in Listings A, Listing B, and Listing C. (You'll need to create a new project that uses the OWL libraries, and add STREAMER.CPP and DCSTREAM.CPP to the project.) When you run the application, you'll see the streamed text appear in the window, as shown in Figure C.


Figure C - The streamed text appears directly in the window's device context.


Summary

At the beginning of this article, I noted that conventional Windows programming requires the use of wsprintf (or sprintf) calls, which translate data types into a character string. This character string is then sent to the device context by calls to TextOut. A similar operation actually occurs when you use the odcstream/dcstreambuf method. The difference is that the buffering and translation occur through a standardized (and in my opinion, easy to use) streamable interface. I don't think that there's any performance improvement or loss associated with using odcstream. If you like streams, you'll probably like this interface.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.